package com.vmj.natives.sun.misc;

import com.vmj.heap.VMObject;
import com.vmj.heap.Slots;
import com.vmj.natives.NativeMethod;
import com.vmj.natives.Registry;
import com.vmj.rtda.Frame;
import com.vmj.rtda.LocalVars;
import com.vmj.rtda.OperandStack;

import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * Created by yangshoujun on 2017/9/17 下午2:27.
 */
public class Unsafe {

    public static final java.lang.String jlClass = "sun/misc/Unsafe";

    public static void init() {
        Registry.register(jlClass, "arrayBaseOffset", "(Ljava/lang/Class;)I", new NativeMethod() {
            public void execute(Frame frame) {
                OperandStack stack = frame.getOperandStack();
                stack.pushInt(0);
            }
        });

        Registry.register(jlClass, "arrayIndexScale", "(Ljava/lang/Class;)I", new NativeMethod() {
            public void execute(Frame frame) {
                OperandStack stack = frame.getOperandStack();
                stack.pushInt(0);
            }
        });

        Registry.register(jlClass, "addressSize", "()I", new NativeMethod() {
            public void execute(Frame frame) {
                OperandStack stack = frame.getOperandStack();
                stack.pushInt(8);
            }
        });

        Registry.register(jlClass, "objectFieldOffset", "(Ljava/lang/reflect/Field;)J", new NativeMethod() {
            public void execute(Frame frame) {
                LocalVars vars = frame.getLocalVars();
                VMObject field = vars.getRef(1);
                int offset = field.getIntVar("slot", "I");
                frame.getOperandStack().pushLong(offset);
            }
        });

        Registry.register(jlClass, "allocateMemory", "(J)J", new NativeMethod() {
            public void execute(Frame frame) {
                LocalVars vars = frame.getLocalVars();
                long bytes = vars.getLong(1);

                long address = allocate(bytes);
                frame.getOperandStack().pushLong(address);
            }
        });

        Registry.register(jlClass, "putLong", "(JJ)V", new NativeMethod() {
            public void execute(Frame frame) {
                mem_putLong(frame);
            }
        });

        Registry.register(jlClass, "freeMemory", "(J)V", new NativeMethod() {
            public void execute(Frame frame) {
                LocalVars vars = frame.getLocalVars();
                long address = vars.getLong(1);
                free(address);
            }
        });

        Registry.register(jlClass, "getByte", "(J)B", new NativeMethod() {
            public void execute(Frame frame) {
                byte[] data = _get(frame);
                byte b = data[0];
                frame.getOperandStack().pushInt(b);
            }
        });

        Registry.register(jlClass, "compareAndSwapInt", "(Ljava/lang/Object;JII)Z", new NativeMethod() {
            public void execute(Frame frame) {
                LocalVars vars = frame.getLocalVars();
                VMObject obj = vars.getRef(1);
                java.lang.Object fields = obj.getData();
                long offset = vars.getLong(2);
                int expected = vars.getInt(4);
                int newVal = vars.getInt(5);

                if(fields instanceof Slots) {
                    Slots slots = (Slots) fields;
                    int oldVal = slots.getInt((int) offset);
                    if(oldVal == expected) {
                        slots.setInt((int) offset, newVal);
                        frame.getOperandStack().pushBoolean(true);
                    }
                } else if(fields instanceof int[]) {
                    int[] ints = (int[]) fields;
                    int oldVal = ints[(int) offset];
                    if(oldVal == expected) {
                        ints[(int) offset] = newVal;
                        frame.getOperandStack().pushBoolean(true);
                    }
                } else {
                    // todo
                    throw new RuntimeException("todo: compareAndSwapInt!");
                }
            }
        });

        Registry.register(jlClass, "getIntVolatile", "(Ljava/lang/Object;J)I", new NativeMethod() {
            public void execute(Frame frame) {
                LocalVars vars = frame.getLocalVars();
                java.lang.Object fields = vars.getRef(1).getData();
                long offset = vars.getLong(2);
                OperandStack stack = frame.getOperandStack();

                if(fields instanceof Slots) {
                    Slots slots = (Slots) fields;
                    stack.pushInt(slots.getInt((int) offset));
                } else if(fields instanceof int[]) {
                    int[] shorts = (int[]) fields;
                    stack.pushInt(shorts[(int) offset]);
                } else {
                    throw new RuntimeException("getInt!");
                }
            }
        });

        Registry.register(jlClass, "compareAndSwapObject", "(Ljava/lang/Object;JLjava/lang/Object;Ljava/lang/Object;)Z", new NativeMethod() {
            public void execute(Frame frame) {
                LocalVars vars = frame.getLocalVars();
                VMObject obj = vars.getRef(1);
                java.lang.Object fields = obj.getData();
                long offset = vars.getLong(2);
                VMObject expected = vars.getRef(4);
                VMObject newVal = vars.getRef(5);

                if(fields instanceof Slots) {
                    boolean swaped = casObj((Slots) fields, offset, expected, newVal);
                    frame.getOperandStack().pushBoolean(swaped);
                } else if(fields instanceof VMObject[]) {
                    boolean swaped = casArr((VMObject[]) fields, offset, expected, newVal);
                    frame.getOperandStack().pushBoolean(swaped);
                } else {
                    // todo
                    throw new RuntimeException("todo: compareAndSwapObject!");
                }
            }
        });

    }

    private static boolean casObj(Slots fields, long offset, VMObject expected, VMObject newVal) {
        VMObject current = fields.getRef((int) offset);
        if(current == expected) {
            fields.setRef((int) offset, newVal);
            return true;
        } else {
            return false;
        }
    }

    private static boolean casArr(VMObject[] obj, long offset, VMObject expected, VMObject newVal) {
        VMObject current = obj[(int) offset];
        if(current == expected) {
            obj[(int) offset] = newVal;
            return true;
        } else {
            return false;
        }

    }

    private static Map<Long, byte[]> _allocated = new HashMap<Long, byte[]>();
    private static long _nextAddress = 64;

    private static void allocateMemory(Frame frame) {
        LocalVars vars = frame.getLocalVars();
        long bytes = vars.getLong(1);
        long address = allocate(bytes);
        frame.getOperandStack().pushLong(address);
    }

    public static void reallocateMemory(Frame frame) {
        LocalVars vars = frame.getLocalVars();
        long address = vars.getLong(1);
        long bytes = vars.getLong(3);
        long newAddress = reallocate(address, bytes);
        frame.getOperandStack().pushLong(newAddress);
    }

    public static void freeMemory(Frame frame) {
        LocalVars vars = frame.getLocalVars();
        long address = vars.getLong(1);
        free(address);
    }

    private static void mem_getByte(Frame frame) {

    }

    private static byte[] _get(Frame frame) {
        LocalVars vars = frame.getLocalVars();
        long address = vars.getLong(1);
        byte[] mem = memoryAt(address);
        return mem;
    }

    private static void mem_putLong(Frame frame) {
        LocalVars vars = frame.getLocalVars();
        long address = vars.getLong(1);
        long value = vars.getLong(3);
        byte[] mem = memoryAt(address);
        ByteBuffer buffer = ByteBuffer.allocate(8);
        buffer.putLong(value);
        buffer.rewind();
        byte[] data = buffer.array();
        _allocated.put(address, data);
    }

    private static void mem_putByte(Frame frame) {
        LocalVars vars = frame.getLocalVars();
        long address = vars.getLong(1);
        int value = vars.getInt(3);
        byte[] mem = memoryAt(address);
        mem[0] = (byte) value;
    }

    private static long allocate(long size) {
        byte[] mem = new byte[(int) size];
        long address = _nextAddress;
        _allocated.put(address, mem);
        _nextAddress += size;
        return address;
    }

    public static long reallocate(long address, long size) {
        if(size == 0) {
            return 0;
        } else if(address == 0) {
            return allocate(size);
        } else {
            byte[] mem = memoryAt(address);
            if(mem.length > size) {
                return address;
            } else {
                _allocated.remove(address);
                long newAddress = allocate(size);
                byte[] newMem = memoryAt(newAddress);
                System.arraycopy(mem, 0, newMem, 0, mem.length);
                return newAddress;
            }
        }
    }

    private static void free(long address) {
        if(_allocated.containsKey(address)) {
            _allocated.remove(address);
        } else {
            throw new RuntimeException("memory was not allocated!");
        }
    }

    public static byte[] memoryAt(long address) {
        Set<Long> keyset = _allocated.keySet();
        for(Long startAddress : keyset) {
            byte[] mem = _allocated.get(startAddress);
            long endAddress = startAddress + mem.length;
            if(address >= startAddress && address < endAddress) {
                long offset = address - startAddress;
                byte[] ret = new byte[(int) (mem.length - offset)];
                System.arraycopy(mem, (int)offset, ret, 0, (int)(mem.length - offset) );
                return ret;
            }
        }
        throw new RuntimeException("invalid address!");
    }

    public static void main(String[] args) {

        ByteBuffer buffer = ByteBuffer.allocate(8);
        buffer.putLong(128);
        buffer.rewind();
//        long l = buffer.getLong();
//        System.out.println(l + "," + Long.MAX_VALUE);
        byte[] b = buffer.array();
        System.out.println(b);
    }
}
